package pl.enigmatic.util;

import static java.lang.reflect.Array.getLength;
import static pl.enigmatic.util.Reflection.arrayBase;
import static pl.enigmatic.util.Reflection.arrayDepth;
import static pl.enigmatic.util.Reflection.isPrimitive;
import static pl.enigmatic.util.Reflection.isStrictPrimitive;
import static pl.enigmatic.util.Reflection.primitive;
import static pl.enigmatic.util.Reflection.wrapper;

import java.lang.reflect.Array;

import pl.enigmatic.PropertyProjector;
import pl.enigmatic.math.MathUtil;
import pl.enigmatic.math.perm.Permutation;
import pl.enigmatic.objects.TypedPropertyProjector;


public class Arrays {

	public static Object get(final Object array, final int... index) {
		if (index.length == 1) {
			return Array.get(array, index[0]);
		} else {
			return get(Array.get(array, index[0]), changeSize(index, index.length - 1, false, 0));
		}
	}

	public static void set(final Object array, final Object value, final int... index) {
		if (index.length == 1) {
			Array.set(array, index[0], value);
		} else {
			set(get(array, changeSize(index, index.length - 1, true, 0)), value, index[index.length - 1]);
		}
	}

	/**
	 * Produces an array of <code>int</code>s with the given <code>length</code>, initialized with numbers from <code>start</code> to
	 * <code>start + shift * (length - 1)</code> with the difference of <code>shift</code> between each two consecutive ones (so the array is linear
	 * with the given <code>start</code> value).
	 * @param length the desired length of an array
	 * @param start the starting value at the first entry of the array
	 * @param shift the difference between two neighbor elements in the array
	 * @return an array of integers initialized as an identity multiplied with the <code>shift</code> value shifted with <code>start</code> value
	 */
	public static int[] linear(final int length, final int start, final int shift) {
		final int[] res = new int[length];
		for (int i = 0; i < res.length; i++) {
			res[i] = start + i * shift;
		}
		return res;
	}

	/**
	 * Produces an array of <code>int</code>s initialized with numbers from <code>start</code> to <code>length + start - 1</code>.
	 * @param length the desired length of an array
	 * @param start the starting value at the first entry of the array
	 * @return an array of integers initialized as an identity shifted with <code>start</code> value
	 */
	public static int[] linear(final int length, final int start) {
		return linear(length, start, 1);
	}

	/**
	 * Produces an array of <code>int</code>s initialized with numbers from <code>0</code> to <code>length - 1</code>
	 * @param length the desired length of an array
	 * @return an array of integers initialized as an identity
	 */
	public static int[] identity(final int length) {
		return linear(length, 0);
	}

	/**
	 * Produces a constant array of the given <code>length</code>.
	 * @param length the length of produced array
	 * @param value the initialization value
	 * @return [value, value, ..., value]
	 * */
	public static int[] constant(final int length, final int value) {
		return linear(length, value, 0);
	}

	/**
	 * Initializes <code>array</code> with the given <code>value</code>.
	 * @param <X> the base type of array element
	 * @param array the array to initialize
	 * @param value the initialization value
	 * @return the same <code>array</code> initialized with the given <code>value</code>
	 */
	public static <X> X[] initialize(final X[] array, final X value) {
		for (int i = 0; i < array.length; i++) {
			array[i] = value;
		}
		return array;
	}

	/**
	 * Produces an array of type <code>boolean</code> initialized with the given <code>value</code>.
	 * @param length the length of produced array
	 * @param value the initialization value
	 * @return [value, value, ..., value]
	 * */
	public static boolean[] constant(final int length, final boolean value) {
		final boolean[] res = new boolean[length];
		for (int i = 0; i < res.length; i++) {
			res[i] = value;
		}
		return res;
	}

	/**
	 * Adds elements of the given array coordinate-wise, and when the lengths are different - the space after the shorter one is filled with values
	 * from the longer one.
	 * @param left the first array
	 * @param right the second array
	 * @return [left[0] + right[0], ... ]
	 * */
	public static int[] add(final int[] left, final int[] right) {
		if (left.length < right.length) {
			return add(right, left);
		} else {
			final int[] res = new int[left.length];
			for (int i = 0; i < right.length; i++) {
				res[i] = left[i] + right[i];
			}
			for (int i = right.length; i < left.length; i++) {
				res[i] = left[i];
			}
			return res;
		}
	}

	public static boolean[] convert(final Boolean[] array) {
		final boolean[] res = new boolean[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Boolean[] convert(final boolean[] array) {
		final Boolean[] res = new Boolean[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static byte[] convert(final Byte[] array) {
		final byte[] res = new byte[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Byte[] convert(final byte[] array) {
		final Byte[] res = new Byte[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static short[] convert(final Short[] array) {
		final short[] res = new short[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Short[] convert(final short[] array) {
		final Short[] res = new Short[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static int[] convert(final Integer[] array) {
		final int[] res = new int[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Integer[] convert(final int[] array) {
		final Integer[] res = new Integer[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static long[] convert(final Long[] array) {
		final long[] res = new long[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Long[] convert(final long[] array) {
		final Long[] res = new Long[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static float[] convert(final Float[] array) {
		final float[] res = new float[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Float[] convert(final float[] array) {
		final Float[] res = new Float[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static double[] convert(final Double[] array) {
		final double[] res = new double[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Double[] convert(final double[] array) {
		final Double[] res = new Double[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static char[] convert(final Character[] array) {
		final char[] res = new char[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Character[] convert(final char[] array) {
		final Character[] res = new Character[array.length];
		for (int i = 0; i < array.length; i++) {
			res[i] = array[i];
		}
		return res;
	}

	public static Object newInstance(final Class<?> base, final int length, final int depth) {
		final int[] dimensions = constant(depth, 0);
		dimensions[0] = length;
		return Array.newInstance(base, dimensions);
	}

	public static Object convert(final Object array) {
		Class<?> clazz;
		if (array == null || !(clazz = array.getClass()).isArray()) { return array; }
		final Class<?> base = arrayBase(clazz);
		if (!isPrimitive(base)) { return array; }
		final int depth = arrayDepth(clazz);
		Class<?> wrap;
		if (isStrictPrimitive(base)) {
			wrap = wrapper(base);
		} else {
			wrap = primitive(base);
		}
		return convert(array, wrap, depth);
	}

	private static Object convert(final Object array, final Class<?> wrapper, final int depth) {
		final int length = getLength(array);
		final Object res = newInstance(wrapper, length, depth);
		if (depth == 1) {
			for (int i = 0; i < length; i++) {
				set(res, get(array, i), i);
			}
		} else {
			for (int i = 0; i < length; i++) {
				set(res, convert(get(array, i), wrapper, depth - 1), i);
			}
		}
		return res;
	}

	public static <X, Y> Y[] convert(final X[] xs, final Y[] ys, final IConverter<X, Y> converter) {
		for (int i = 0; i < ys.length; i++) {
			ys[i] = converter.convert(xs[i]);
		}
		return ys;
	}

	public static int clamp(final Object[] array, final int index) {
		return MathUtil.clamp(0, array.length, index);
	}

	public static int wrap(final Object[] array, final int index) {
		return MathUtil.wrap(0, array.length, index);
	}

	@SuppressWarnings("unchecked")
	public static <X> X[] concatenate(final X[]... arrays) {
		int size = 0;
		for (final X[] x : arrays) {
			size += x.length;
		}
		final X[] res = (X[]) Array.newInstance(arrays.getClass().getComponentType().getComponentType(), size);
		int i = 0;
		for (final X[] a : arrays) {
			for (final X x : a) {
				res[i++] = x;
			}
		}
		return res;
	}

	public static boolean[] concatenate(final boolean[]... arrays) {
		final Boolean[][] transformed = new Boolean[arrays.length][];
		for (int i = 0; i < arrays.length; i++) {
			transformed[i] = convert(arrays[i]);
		}
		return convert(concatenate(transformed));
	}

	public static byte[] concatenate(final byte[]... arrays) {
		final Byte[][] transformed = new Byte[arrays.length][];
		for (int i = 0; i < arrays.length; i++) {
			transformed[i] = convert(arrays[i]);
		}
		return convert(concatenate(transformed));
	}

	public static char[] concatenate(final char[]... arrays) {
		final Character[][] transformed = new Character[arrays.length][];
		for (int i = 0; i < arrays.length; i++) {
			transformed[i] = convert(arrays[i]);
		}
		return convert(concatenate(transformed));
	}

	public static double[] concatenate(final double[]... arrays) {
		final Double[][] transformed = new Double[arrays.length][];
		for (int i = 0; i < arrays.length; i++) {
			transformed[i] = convert(arrays[i]);
		}
		return convert(concatenate(transformed));
	}

	public static float[] concatenate(final float[]... arrays) {
		final Float[][] transformed = new Float[arrays.length][];
		for (int i = 0; i < arrays.length; i++) {
			transformed[i] = convert(arrays[i]);
		}
		return convert(concatenate(transformed));
	}

	public static int[] concatenate(final int[]... arrays) {
		final Integer[][] transformed = new Integer[arrays.length][];
		for (int i = 0; i < arrays.length; i++) {
			transformed[i] = convert(arrays[i]);
		}
		return convert(concatenate(transformed));
	}

	public static long[] concatenate(final long[]... arrays) {
		final Long[][] transformed = new Long[arrays.length][];
		for (int i = 0; i < arrays.length; i++) {
			transformed[i] = convert(arrays[i]);
		}
		return convert(concatenate(transformed));
	}

	public static short[] concatenate(final short[]... arrays) {
		final Short[][] transformed = new Short[arrays.length][];
		for (int i = 0; i < arrays.length; i++) {
			transformed[i] = convert(arrays[i]);
		}
		return convert(concatenate(transformed));
	}

	public static <X, V> X fill(final X array, final int start, final int end, final V val) {
		for (int i = start; i <= end; i++) {
			set(array, val, i);
		}
		return array;
	}

	public static <X, Y> Y copy(final X src, final Y trg, final boolean alignLeft, int offset) {
		final int old = getLength(src);
		final int length = getLength(trg);
		if (!alignLeft) {
			offset = length - old - offset;
		}
		final int from;
		final int to;
		if (offset >= 0) {
			from = 0;
			to = offset;
		} else {
			from = -offset;
			to = 0;
		}
		final int count = MathUtil.min(old, length - to + from);
		for (int i = from; i < count; i++) {
			set(trg, get(src, i), to + i - from);
		}
		return trg;
	}

	public static <X, Y> Y copy(final X src, final Y trg) {
		return copy(src, trg, true, 0);
	}

	@SuppressWarnings("unchecked")
	public static <X> X changeSize(final X array, final int length, final boolean alignLeft, final int offset) {
		return copy(array, (X) Array.newInstance(array.getClass().getComponentType(), length), alignLeft, offset);
	}

	public static <X> X changeSize(final X array, final int length) {
		return changeSize(array, length, true, 0);
	}

	public static Object toStrings(final Object array) {
		if (array == null) { return null; }
		if (!array.getClass().isArray()) { return array.toString(); }
		final Object strings = newInstance(String.class, getLength(array), arrayDepth(array.getClass()));
		for (int i = 0; i < getLength(array); i++) {
			set(strings, toStrings(get(array, i)), i);
		}
		return strings;
	}

	@SuppressWarnings("unchecked")
	public static <X, Y> Y permute(final X src, final Y trg, final Permutation sigma) {
		final int length = getLength(trg);
		final X tmp = (X) Array.newInstance(trg.getClass().getComponentType(), length);
		for (int i = 0; i < length; i++) {
			set(tmp, get(src, sigma.at(i)), i);
		}
		return copy(tmp, trg);
	}

	public static int[] getDimensions(Object array) {
		if (array == null) { return null; }
		Class<?> cls = array.getClass();
		if (!cls.isArray()) {
			return new int[0];
		} else {
			final int[] res = new int[arrayDepth(cls)];
			int i = 0;
			while (cls.isArray()) {
				res[i++] = getLength(array);
				array = get(array, 0);
				cls = cls.getComponentType();
			}
			return res;
		}
	}

	@SuppressWarnings("unchecked")
	public static <X> X transpone(final X array, final Permutation sigma) {
		final int[] dim = getDimensions(array);
		final X res = (X) Array.newInstance(arrayBase(array.getClass()), //
				concatenate(//
						permute(changeSize(dim, sigma.length(), true, 0), new int[sigma.length()], sigma), //
						changeSize(dim, dim.length - sigma.length(), true, -sigma.length()))//
				);
		final int[] index = constant(sigma.length(), 0);
		final int[] tmp = new int[sigma.length()];
		final int x = index.length - 1;
		while (true) {
			set(res, get(array, index), permute(index, tmp, sigma));
			if (++index[x] == dim[x]) {
				int s = x;
				while (s >= 0 && index[s] >= dim[s] - 1) {
					index[s] = 0;
					s--;
				}
				if (s < x) {
					if (s >= 0) {
						index[s]++;
					} else {
						return res;
					}
				}
			}
		}
	}

	public static <X> X transpone(final X array, final int... perm) {
		return transpone(array, new Permutation(perm));
	}

	public static <X> X transpone(final X array) {
		return transpone(array, 1, 0);
	}

	public static <X> X[][] transpone(final X[][] array) {
		return transpone(array, 1, 0);
	}

	private static <X, Y> Y[] extract(final X[] base, final PropertyProjector<X, Y> projector, final Y[] res, final boolean notNull) {
		final int max = Math.min(base.length, res.length);
		for (int i = 0; i < max; i++) {
			final X obj = base[i];
			if (notNull && obj == null) {
				res[i] = null;
			} else {
				res[i] = projector.project(obj);
			}
		}
		return res;
	}

	public static <X, Y> Y[] extract(final X[] base, final PropertyProjector<X, Y> projector, final Y[] res) {
		return extract(base, projector, res, true);
	}

	public static <X, Y> Y[] extractNotNull(final X[] base, final PropertyProjector<X, Y> projector, final Y[] res) {
		return extract(base, projector, res, false);
	}

	@SuppressWarnings("unchecked")
	public static <X, Y> Y[] extract(final X[] base, final TypedPropertyProjector<X, Y> extractor) {
		return extract(base, extractor, (Y[]) Array.newInstance(extractor.getPropertyType(), base.length));
	}

	@SuppressWarnings("unchecked")
	public static <X, Y> Y[] extractNotNull(final X[] base, final TypedPropertyProjector<X, Y> extractor) {
		return extractNotNull(base, extractor, (Y[]) Array.newInstance(extractor.getPropertyType(), base.length));
	}

	public static <S, T> T[] transform(final S[] src, final Transformer<S, T> t, final T[] trg) {
		for (int i = 0; i < src.length; i++) {
			trg[i] = t.transform(src[i]);
		}
		return trg;
	}

	@SuppressWarnings("unchecked")
	public static <X> X inverse(final X array) {
		final int length = Array.getLength(array);
		final X res = (X) Array.newInstance(array.getClass().getComponentType(), length);
		for (int i = 0; i < length; i++) {
			set(res, get(array, i), length - i - 1);
		}
		return res;
	}

}
